package peer.SharingManager;


import java.io.DataOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import peer.BitField;
import peer.NetworkManager.NetworkManager;
import peer.gui.PeerGUI;
import common.*;
/**
 * @author Andrew Nicholson
 * Facade for the SharingManager component. Makes all of the component's services available as public methods, while hiding
 * the implementation from the rest of the system
 */
public class SharingManager 
{

	private Map<String, FileReference> sharedFiles;
	
	
	private static SharingManager Singleton;
	
	private LinkedList<Long> searches = new LinkedList<Long>(); 

	/**
	 * private constructor - only called by the instance method.
	 * Initialises and loads the list of files it can share, identified
	 * by the hash code.
	 * @throws Exception
	 */
	private SharingManager() throws Exception
	{

		sharedFiles = Collections.synchronizedMap(new HashMap<String, FileReference>());
		loadFiles();
	}
	
	/**
	 * Loads the shared file information into memory. Comments
	 * in the file are demarcated by a '#' at the start of the line
	 * @throws Exception
	 */
	private void loadFiles() throws Exception
	{
		String dir = System.getProperty("user.dir");
		File path = new File(dir);
		
		for(File f: path.listFiles(new SharingFilter()))
		{
			if(f.isFile())
				addExistingFile(f.getName());
		}
		
		

	}

	/** Returns the instance of the SharingManager. If it hasn't been created,
	 * then instantiates the object. 
	 * 
	 * @return A reference to the SharingManager component
	 */
	public static SharingManager instance ()
	{
		try
		{
			if(Singleton == null)
				Singleton = new SharingManager();
			
			return Singleton;
		}
		catch (Exception e)
		{
			throw new Error("Could not create Sharing Manager!");
		}
	}
	
	/**
	 * Remove a file from the list of files to share
	 * @param hashCode The hashcode of the file to remove
	 * @return true if the operation was successful
	 */
	public boolean removeFile(String hashCode)
	{
		if(sharedFiles.containsKey(hashCode))
			sharedFiles.remove(hashCode);
		
		return true;
	}

	/**
	 * Removes a file from the list of files to share
	 * @param reference a FileReference object
	 * @return true if the operation was successful
	 */
	public boolean removeFile(FileReference reference)
	{
		if(sharedFiles.containsKey(reference.getHashCode()))
			sharedFiles.remove(reference.getHashCode());
		
		return true;
	}

	/**
	 * Finds files that match the search string, and returns a list of the matches
	 * @param string A regular expression to match on
	 * @return A collection of matching files - if there were none
	 * returns an empty object (but not null)
	 */
	public synchronized ArrayList<FileReference> findMatches(String string)
	{
		ArrayList<FileReference> resultSet = new ArrayList<FileReference>();
		
		Set<String> keys = sharedFiles.keySet();
		
		for(String s: keys)
		{
			if(sharedFiles.get(s).getFileName().matches(".*" + string + ".*"))
			{
				resultSet.add(sharedFiles.get(s));
			}
		}

		
		return resultSet;
	}

	/**
	 * Add a file to the list of shared files that we know is already stored on the filesystem
	 * in its entirety.
	 * @param name The path to the file
	 * @return true if the operation was successful, otherwise returns false
	 */
	public synchronized boolean addExistingFile(String name)
	{
		try
		{
			File file = new File(name);
			FileReference newFile = new FileReference(file, file.length());
			sharedFiles.put(newFile.getHashCode(), newFile);

			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * Add a file to the list of shared files that is not completely downloaded.
	 * @param name the name of the file
	 * @param filesize the size of the file
	 * @param hashCode the hashcode the file will have once downloaded
	 * @param bitfield which chunks of the file do we actually have?
	 * @return true if the operation was successful, otherwise returns false
	 * @throws Exception
	 */
	public synchronized boolean addNewFile(String name, long filesize, String hashCode, BitField bitfield) throws Exception
	{
		try
		{
			FileReference toAdd = new FileReference(name, filesize, hashCode, bitfield);
			sharedFiles.put(hashCode, toAdd);
			
			
			return true;
		}
		catch (Exception e)
		{
			return false;
		}
		
	}
	
	private class SharingFilter implements FilenameFilter
	{
		private List<String> blackList;
		
		SharingFilter()
		{
			blackList = new ArrayList<String>();
			blackList.add("class");
			blackList.add("java");			
		}
		
		public boolean accept(File dir, String name)
		{
			String[] tokens = name.split("\\.+");
			String extension = tokens[tokens.length - 1];
			
			for(String s: blackList)
			{
				if(s.equalsIgnoreCase(extension))
					return false;
			}
			return true;
		}
		
	
	}
	
	private FileReference getFile(String name)
	{
		return sharedFiles.get(name);
		//TODO: this

	}
	
	
	/**Who called this getPacket ? It doesn't even return anything...
	 * It is used to give a packet to this (the sharing manager)
	 * @param p
	 * @param fromAddress
	 */
	public void getPacket(Packet p, InetSocketAddress fromAddress)
	{
		switch(p.getType())
		{
		case 0x10: //search
			propogateSearch(p, fromAddress);
			searchReply(p, fromAddress);
			break;
		case 0x18: //request_file
			file_request_reply(p, fromAddress);
			break;
		case 0x1C: //status_request
			status_request_reply(p, fromAddress);
			break;
		case 0x24: //request_block
			send_data(p, fromAddress);
			break;
		case 0x28: //data
			break;
		default:
			break;
		}
	}

	private void send_data(Packet p, InetSocketAddress fromAddress)
	{
		FileReference f = getFile(p.getHeader("hash_code"));
		//p.addHeader("block_checksum", "not implemented");
		
		try
		{
			Socket socket = new Socket(fromAddress.getAddress(), Integer.parseInt(p.getHeader("port")));
			byte[] temp = f.readBlock(Integer.parseInt(p.getHeader("block")));
			//socket.getOutputStream().write(f.readBlock(Integer.parseInt(p.getHeader("block"))));
			DataOutputStream out = new DataOutputStream(socket.getOutputStream());
			out.write(temp);
			socket.close();
		} catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		
	}

	public void propogateSearch(Packet p, InetSocketAddress fromAddress)
	{
		//decrement the die in count
		int dieIn = Integer.parseInt(p.getHeader("die_in")) - 1;
		p.removeHeader("die_in");
		p.addHeader("die_in", Integer.toString(dieIn));
		//add this search to the list of searches propogated
		Long searchId = Long.parseLong(p.getHeader("search_id"));
		if (!searches.contains(searchId))
		{
			searches.add(searchId);
			//and send it to each connected peer
			NetworkManager nm = NetworkManager.instance();
			for (InetSocketAddress peer : nm.getPeerList())
			{
				if(peer != fromAddress)
				{
					nm.sendPeerPacket(p, peer);
				}
			}
		}
	}

	private void file_request_reply(Packet p, SocketAddress fromAddress) {
		// TODO Auto-generated method stub
		
	}

	private void status_request_reply(Packet p, InetSocketAddress fromAddress) 
	{
		Packet replyPacket = new Packet();
		replyPacket.setType(Packet.Type.status_response);
		replyPacket.addHeader("name", p.getHeader("name"));
		replyPacket.addHeader("file_size", p.getHeader("file_size"));
		replyPacket.addHeader("hash_code", p.getHeader("hash_code"));
		replyPacket.addHeader("upload_slot", "yes");
		
		FileReference f = sharedFiles.get(p.getHeader("hash_code"));
		
		replyPacket.setData(f.getBitField().toString());
		
		NetworkManager nm = NetworkManager.instance();
		
		nm.sendPeerPacket(replyPacket, fromAddress);
		
	}

	private void searchReply(Packet p, SocketAddress fromAddress)
	{
		NetworkManager nm = NetworkManager.instance();
		ArrayList<FileReference> matches = findMatches(p.getHeader("query"));
		for(FileReference match: matches)
		{
			Packet replyPacket = new Packet();
			replyPacket.setType(Packet.Type.found);
			replyPacket.addHeader("query", p.getHeader("query")); //not according to protocol - it should be identified by search_id
			replyPacket.addHeader("for", p.getHeader("for"));
			replyPacket.addHeader("search_id", p.getHeader("search_id"));
			replyPacket.addHeader("name", match.getFileName());
			replyPacket.addHeader("file_size", Long.toString(match.getFileSize()));
			replyPacket.addHeader("hash_code", match.getHashCode());
			
			String[] destination = p.getHeader("for").split(":");
			
			try {
				//nm.sendPeerPacket(replyPacket, InetSocketAddress.createUnresolved(destination[0], Integer.parseInt(destination[1])));
				nm.sendPeerPacket(replyPacket, new InetSocketAddress(destination[0], Integer.parseInt(destination[1])));
			} catch (NumberFormatException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			
			
		}
		// TODO Auto-generated method stub
		
	}
	
}
